// -------------------- iSharePoint -------------------- //
// 
//  Company:	IT Complex, LLC
//  Project:	Smart.Common
//  File name:	ParsingExts.cs
//  Developer:	Igor Solomatov
//  Created:	03.03.2013
// 
// -------------------- iSharePoint -------------------- //

using System;
using System.Collections.Generic;

namespace Smart
{
	public static class ParsingExts
	{
		public static char SingleQuote = '\'';
		public static char OpenBracket = '(';
		public static char CloseBracket = ')';
		public static char Comma = ',';
		public static char Semicolon = ';';

		public static char Assignment = '=';
		public static char Gt = '>';
		public static char Lt = '<';
		public static string Geq = ">=";
		public static string Leq = "<=";
		public static string Neq = "!=";

		public static char[] ComparisonOperations = new[] { Assignment, Gt, Lt, '!' };
		public static char[] WhiteSpace = new[] { ' ', '\t' };
		public static char[] NewLine = new[] { '\r', '\n' };
		public static char[] WhiteSpaceWithNewLine = new[] { ' ', '\t', '\r', '\n' };

		/// <summary>
		/// Find quote end
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="startIdx">Index of first single quote</param>
		/// <param name="endIdx">Last index of string block</param>
		/// <param name="value">Value without quotes and with replaced doubled quotes</param>
		/// <returns>Index of last single quote</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		public static int FindQuoteEnd(this string src, int startIdx, int endIdx, out string value)
		{
			if (src == null)
				throw new ArgumentNullException("src");

			var quote = src[startIdx];

			var cidx = startIdx + 1;

			value = null;

			// For each quote char, until end quote found
			while (cidx <= endIdx)
			{
				// Find single quote
				var idx = src.IndexOf(quote, cidx);

				// Not found or out of bounds? End quote should be found
				if (idx < 0 || idx > endIdx)
					return -1;

				// Whether next char is single quote?
				if (idx + 1 <= endIdx && src[idx + 1] == quote)
				{
					// Continue
					cidx = idx + 2;
				}
				else
				{
					cidx = idx;
					break;
				}
			}

			// Out of bounds? End quote should be found
			if (cidx > endIdx)
				return -1;

			// Get value
			value =
				cidx - startIdx - 1 <= 0
					? String.Empty
					: src.Substring(startIdx + 1, cidx - startIdx - 1).Replace(new string(quote, 2), new string(quote, 1));

			return cidx;
		}

		/// <summary>
		/// Find parameter end
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="startIdx">First char position of parameter</param>
		/// <param name="endIdx">Last char position of string block</param>
		/// <param name="paramQuote">Parameter quote char (usually "'")</param>
		/// <param name="paramSeparator">Parameter separator char (usually comma)</param>
		/// <param name="paramTerminator">Parameter terminator char (usually close bracket)</param>
		/// <param name="value">Parameter value (trimmed)</param>
		/// <returns>Index of separator or terminator chars</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		public static int FindParamEnd(this string src, int startIdx, int endIdx, char paramQuote, char paramSeparator, char paramTerminator, out string value)
		{
			if (src == null)
				throw new ArgumentNullException("src");

			value = null;

			var cidx = src.SkipI(WhiteSpaceWithNewLine, startIdx, endIdx);

			if (cidx < 0)
				return -1;

			var ff = src[cidx];

			// Quote?
			if (ff == paramQuote)
			{
				string paramValue;

				var qidx = FindQuoteEnd(src, cidx, endIdx, out paramValue);

				if (qidx < 0)
					return -1;

				cidx = qidx + 1;
				value = paramValue;
			}
			// Parameter value?
			else if (Char.IsLetterOrDigit(ff))
			{
				// Find parameter end
				var idx = src.SkipWhile(Char.IsLetterOrDigit, cidx, endIdx);

				// Not found or out of bounds? Every parameter should end with separator or terminator
				if (idx < 0 || idx > endIdx)
					return -1;

				value = src.Substring(cidx, idx - cidx).Trim();

				cidx = idx;
			}

			// WhiteSpace or NewLine?
			if (src[cidx].In(WhiteSpaceWithNewLine))
			{
				var tsidx = src.SkipI(WhiteSpaceWithNewLine, cidx, endIdx);

				// out of bounds? Every parameter should end with separator or terminator
				if (tsidx < 0)
					return -1;

				cidx = tsidx;
			}

			var end = src[cidx];

			// Every parameter should end with separator or terminator
			if (end.Out(paramSeparator, paramTerminator))
				return -1;

			return cidx;
		}

		/// <summary>
		/// Find parameters block end
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="startIdx">First char position of parameter</param>
		/// <param name="endIdx">Last char position of string block</param>
		/// <param name="paramQuote">Parameter quote char (usually ')</param>
		/// <param name="paramSeparator">Parameter separator char (usually comma)</param>
		/// <param name="paramTerminator">Parameter terminator char (usually close bracket)</param>
		/// <param name="values">Parameters' values (trimmed)</param>
		/// <returns>Index of terminator char</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		public static int FindParamsEnd(this string src, int startIdx, int endIdx, char paramQuote, char paramSeparator, char paramTerminator, out string[] values)
		{
			if (src == null)
				throw new ArgumentNullException("src");

			var paramValues = new List<string>();

			var cidx = startIdx;

			values = null;

			// For each parameter until terminator found
			while (cidx <= endIdx)
			{
				string paramValue;

				// Find single parameter
				var idx = FindParamEnd(src, cidx, endIdx, paramQuote, paramSeparator, paramTerminator, out paramValue);

				// Not found or out of bounds? Every parameter should end with separator or terminator 
				if (idx < 0)
					return -1;

				// Add parameter value
				if (!paramValue.IsNullOrEmpty())
					paramValues.Add(paramValue);

				var ts = src[idx];

				// Separator?
				if (ts == paramSeparator)
				{
					cidx = idx + 1;
				}
				// Terminator?
				else if (ts == paramTerminator)
				{
					cidx = idx;

					break;
				}
				// Other characters not allowed!
				else
				{
					return -1;
				}
			}

			// Out of bounds? Parameters block should end with terminator 
			if (cidx > endIdx)
				return -1;

			values = paramValues.ToArray();

			return cidx;
		}

		/// <summary>
		/// Find parameters block end
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="startIdx">First char position of parameter</param>
		/// <param name="endIdx">Last char position of string block</param>
		/// <param name="paramQuote">Parameter quote char (usually "'")</param>
		/// <param name="paramSeparator">Parameter separator char (usually comma)</param>
		/// <param name="methodOpenParenthesis">Open parenthesis (usually "(")</param>
		/// <param name="methodCloseParenthesis">Close parenthesis (usually ")")</param>
		/// <param name="name">Method name</param>
		/// <param name="args">Method's arguments (trimmed)</param>
		/// <returns>Index of next char after method</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		public static int FindMethodEnd(this string src, int startIdx, int endIdx, char paramQuote, char paramSeparator, char methodOpenParenthesis, char methodCloseParenthesis, out string name, out string[] args, out bool containsParenthesis)
		{
			if (src == null)
				throw new ArgumentNullException("src");

			name = null;
			args = null;

			containsParenthesis = false;

			// find start of name
			var nsidx = src.SkipI(WhiteSpaceWithNewLine, startIdx, endIdx);

			// not found?
			if (nsidx < 0)
				return -1;

			// find end of name
			var neidx = src.SkipWhile(Char.IsLetterOrDigit, nsidx, endIdx);

			// all string is name, no args
			if (neidx < 0)
			{
				// only name, no args
				name = src.Substring(nsidx).Trim();
				args = new string[] { };

				if (name.IsNullOrEmpty())
					return -1;

				return endIdx + 1;
			}

			// Name should be!
			if (neidx - nsidx <= 0)
				return -1;

			name = src.Substring(nsidx, neidx - nsidx);

			var psidx = src.SkipI(WhiteSpaceWithNewLine, neidx, endIdx);

			// find end, no args
			if (psidx < 0)
			{
				args = new string[] { };

				return endIdx + 1;
			}

			var op = src[psidx];

			// Should be
			if (op != methodOpenParenthesis)
				return psidx;

			containsParenthesis = true;

			var peidx = FindParamsEnd(src, psidx + 1, endIdx, paramQuote, paramSeparator, methodCloseParenthesis, out args);

			// Should end with methodCloseParenthesis
			if (peidx < 0 || src[peidx] != methodCloseParenthesis)
				return -1;

			return peidx + 1;
		}

		/// <summary>
		/// Adds function to method block
		/// </summary>
		public delegate void FunctionAdderHandler<TMethodBlock>(TMethodBlock methodBlock, string name, string[] parameters, bool containsParenthesis);

		/// <summary>
		/// Parse whole <paramref name="src"/> as methods list, separated with <paramref name="paramSeparator"/>
		/// </summary>
		/// <param name="src">Source string to parse</param>
		/// <param name="paramQuote">Quote char</param>
		/// <param name="paramSeparator">Method and paramter separator char</param>
		/// <param name="methodOpenParenthesis">Open method parenthesis char</param>
		/// <param name="methodCloseParenthesis">Close method parenthesis char</param>
		/// <param name="methodBlock">Stores methods</param>
		/// <param name="functionAdder">Adds method to <paramref name="methodBlock"/></param>
		/// <returns>true, if succeeded</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		/// <exception cref="InvalidCastException">Method or Separator</exception>
		public static bool ParseMethods<TMethodBlock>(this string src, char paramQuote, char paramSeparator, char methodOpenParenthesis, char methodCloseParenthesis, TMethodBlock methodBlock, FunctionAdderHandler<TMethodBlock> functionAdder)
		{
			if (src == null) throw new ArgumentNullException("src");

			string mn;
			string[] mp;
			bool cp;

			var idx = 0;
			var endIdx = src.Length - 1;

			while (idx < endIdx)
			{
				var methodEndIdx = FindMethodEnd(src, idx, endIdx, paramQuote, paramSeparator, methodOpenParenthesis, methodCloseParenthesis, out mn, out mp, out cp);

				if (methodEndIdx < 0)
					throw new InvalidCastException("Method");

				functionAdder(methodBlock, mn, mp, cp);

				methodEndIdx = src.SkipI(WhiteSpaceWithNewLine, methodEndIdx, endIdx);

				if (methodEndIdx < 0)
					break;

				if (src[methodEndIdx] != paramSeparator)
					throw new InvalidCastException("Separator");

				idx = methodEndIdx + 1;
			}

			return true;
		}

		/// <summary>
		/// Find corresponding parenthesis end
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="startIdx">Current block start</param>
		/// <param name="endIdx">Current block end</param>
		/// <param name="paramQuote">Parameter quote char (usually "'")</param>
		/// <param name="openParenthesis">Open parenthesis (usually "(")</param>
		/// <param name="closeParenthesis">Close parenthesis (usually ")")</param>
		/// <param name="value">Internal value in group</param>
		/// <returns>Index of closeParenthesis char</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		public static int FindParenthesisEnd(this string src, int startIdx, int endIdx, char paramQuote, char openParenthesis, char closeParenthesis, out string value)
		{
			if (src == null)
				throw new ArgumentNullException("src");

			var searchChars = new[] { paramQuote, openParenthesis, closeParenthesis };

			var cidx = startIdx;
			var plevel = 1;

			value = null;

			// For each quote or openParenthesis or closeParenthesis char, until end quote found
			while (cidx <= endIdx)
			{
				// Find quote or openParenthesis or closeParenthesis char
				var idx = src.IndexOfAny(searchChars, cidx);

				// Not found or out of bounds? Should end with closeParenthesis
				if (idx < 0 || idx > endIdx)
					return -1;

				var ac = src[idx];

				// Quoted string?
				if (ac == paramQuote)
				{
					string tmp;

					var qidx = FindQuoteEnd(src, idx, endIdx, out tmp);

					// Should be end quote
					if (qidx < 0)
						return -1;

					cidx = qidx + 1;
				}
				// New parenthesis group?
				else if (ac == openParenthesis)
				{
					plevel++;

					cidx = idx + 1;
				}
				// End of parenthesis group?
				else if (ac == closeParenthesis)
				{
					cidx = idx;

					// Current level end
					if (plevel-- <= 1)
						break;

					cidx++;
				}
			}

			// Out of bounds? Should end with closeParenthesis
			if (cidx > endIdx || src[cidx] != closeParenthesis)
				return -1;

			// Internal value
			value =
				cidx - startIdx <= 0
					? String.Empty
					: src.Substring(startIdx, cidx - startIdx);

			return cidx;
		}

		/// <summary>
		/// Detect block type
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="idx">Current position</param>
		/// <param name="startIdx">Current block start</param>
		/// <param name="endIdx">Current block end</param>
		/// <param name="block">Current block instance</param>
		/// <returns>-1 to stop; 0 to skip</returns>
		public delegate int DetectorHandler<TBlockType>(string src, int idx, int startIdx, int endIdx, TBlockType block);

		/// <summary>
		/// Parse block type
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="idx">Current position</param>
		/// <param name="blockType">Detected block type</param>
		/// <param name="startIdx">Current block start</param>
		/// <param name="endIdx">Current block end</param>
		/// <param name="block">Current block instance</param>
		/// <returns>Index next after parsed blocked, -1 to stop</returns>
		public delegate int ParserHandler<TBlockType>(string src, int idx, int blockType, int startIdx, int endIdx, TBlockType block);

		/// <summary>
		/// Add child block
		/// </summary>
		public delegate TBlockType ChildGroupAdderHandler<TBlockType>(TBlockType block);

		/// <summary>
		/// Parse block in one iteration
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="startIdx">Current block start idx (idx after parenthesis)</param>
		/// <param name="endIdx">Current block end</param>
		/// <param name="openParenthesis">Open parenthesis (usually "(")</param>
		/// <param name="closeParenthesis">Close parenthesis (usually ")")</param>
		/// <param name="block">Current block</param>
		/// <param name="detector">Detect block type</param>
		/// <param name="parser">Parse block</param>
		/// <param name="adder">Add child block to current block</param>
		/// <returns>Index of next char after closeParenthesis</returns>
		/// <exception cref="ArgumentNullException"><paramref name="src"/> is null</exception>
		public static int ParseBlock<TBlockType>(this string src, int startIdx, int endIdx, char openParenthesis, char closeParenthesis, TBlockType block, DetectorHandler<TBlockType> detector, ParserHandler<TBlockType> parser, ChildGroupAdderHandler<TBlockType> adder)
		{
			if (src == null)
				throw new ArgumentNullException("src");

			var cidx = startIdx;

			// for each block
			while (cidx <= endIdx)
			{
				// Skip all whitespace or new line chars
				var idx = src.SkipI(WhiteSpaceWithNewLine, cidx, endIdx);

				if (idx < 0)
					return endIdx + 1;

				// New block
				if (src[idx] == openParenthesis)
				{
					// Recurrence block
					var bidx = ParseBlock(src, idx + 1, endIdx, openParenthesis, closeParenthesis, adder(block), detector, parser, adder);

					if (bidx < 0)
						return -1;

					if (bidx > endIdx)
						return bidx;

					cidx = bidx + 1;
				}
				// End of current block
				else if (src[idx] == closeParenthesis)
				{
					cidx = idx;

					break;
				}
				// Some other block
				else
				{
					var blockType = detector(src, idx, startIdx, endIdx, block);

					if (blockType < 0)
						return -1;

					if (blockType == 0)
					{
						cidx = idx + 1;
					}
					else
					{
						var bidx = parser(src, idx, blockType, startIdx, endIdx, block);

						if (bidx < 0)
							return -1;

						if (bidx > endIdx)
							return bidx;

						cidx = bidx;
					}
				}
			}

			// Should end with closeParenthesis
			if ((cidx > endIdx || src[cidx] != closeParenthesis) && (startIdx > 0))
				return -1;

			return cidx;
		}

		///<summary>
		///Check if string is logic Operator
		///</summary>
		///<param name="str">String to check</param>
		///<returns></returns>
		public static bool IsLogicOperation(this string str)
		{
			return str.In("and", "or");
		}


		/// <summary>
		/// Cast string to logic operator
		/// </summary>
		/// <param name="str">String to cast</param>
		/// <returns>ConditionGroupJoin type</returns>
		/// <exception cref="InvalidCastException"><paramref name="str"/> is incorrect</exception>
		public static ComparisonOperator CastToLogicOperation(this string str)
		{
			var op = str.SToE(ComparisonOperator.None);

			if (op == ComparisonOperator.And || op == ComparisonOperator.Or)
				return op;

			throw new InvalidCastException("str");
		}

		/// <summary>
		/// Parse comparison block
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="idx">Current block start idx (idx after parenthesis)</param>
		/// <param name="endIdx">Current block end</param>
		/// <param name="operand1">Identified operand1</param>
		/// <param name="op">Condition operator between operands</param>
		/// <param name="operand2">Identified operand2</param>
		/// <returns>Index next after parsed blocked, -1 to stop</returns>
		public static int ParseComparisonBlock(this string src, int idx, int endIdx, out string operand1, out ComparisonOperator op, out string operand2)
		{
			operand1 = null;
			operand2 = null;
			op = ComparisonOperator.None;

			int opIdx;

			// Skip whitespace
			var cidx = src.SkipI(WhiteSpaceWithNewLine, idx, endIdx);

			// All is empty?
			if (cidx < 0)
				return -1;

			// Starts with quote?
			if (src[cidx] == SingleQuote)
			{
				opIdx = FindQuoteEnd(src, cidx, endIdx, out operand1);

				if (opIdx >= 0)
					opIdx++;
			}
			// Starts regularly
			else
			{
				opIdx = src.SkipWhile(Char.IsLetterOrDigit, cidx, endIdx);

				if (opIdx > 0 && opIdx - cidx > 0)
				{
					operand1 = src.Substring(cidx, opIdx - cidx);
				}
			}

			// Method or Quote invalid format
			if (opIdx < 0)
				return -1;

			// Skip whitespace after operand1
			opIdx = src.SkipI(WhiteSpaceWithNewLine, opIdx, endIdx);

			// Ended and not function?
			if (opIdx < 0)
				return -1;

			var opC = src[opIdx];

			if (opC.In(ComparisonOperations))
			{
				if (opIdx + 1 <= endIdx)
				{
					var compOp = src.Substring(opIdx, 2);

					if (compOp == Geq)
					{
						op = ComparisonOperator.Geq;
					}
					else if (compOp == Leq)
					{
						op = ComparisonOperator.Leq;
					}
					else if (compOp == Neq)
					{
						op = ComparisonOperator.Neq;
					}

					if (op != ComparisonOperator.None)
						opIdx += 2;
				}

				if (op == ComparisonOperator.None)
				{
					if (opC == Assignment)
						op = ComparisonOperator.Eq;
					else if (opC == Gt)
						op = ComparisonOperator.Gt;
					else if (opC == Lt)
						op = ComparisonOperator.Lt;
					else
						return -1;

					opIdx++;
				}
			}

			idx = src.SkipI(WhiteSpaceWithNewLine, opIdx, endIdx);

			if (idx < 0)
				return -1;

			var fco2 = src[idx];

			if (fco2 == SingleQuote)
			{
				var op2Idx = FindQuoteEnd(src, src.IndexOf(SingleQuote, idx), endIdx, out operand2);

				if (op2Idx < 0)
					return -1;

				idx = op2Idx + 1;
			}
			else
			{
				var op2Idx = src.SkipWhile(Char.IsLetterOrDigit, idx, endIdx);

				if (op2Idx < 0)
				{
					operand2 = src.Substring(idx, endIdx - idx + 1);

					return endIdx + 1;
				}

				operand2 = src.Substring(idx, op2Idx - idx);

				idx = op2Idx;
			}

			return idx;
		}

		/// <summary>
		/// Parse block type
		/// </summary>
		/// <param name="src">string to search in</param>
		/// <param name="idx">Current block start idx (idx after parenthesis)</param>
		/// <param name="endIdx">Last char index</param>
		/// <param name="op">Identified operator</param>
		/// <returns>Last index of parsed block</returns>
		public static int ParseLogicOperation(this string src, int idx, int endIdx, out ComparisonOperator op)
		{
			op = ComparisonOperator.None;

			var sIdx = src.SkipI(WhiteSpaceWithNewLine, idx, endIdx);

			if (sIdx < 0)
				return -1;

			var eIdx = src.SkipWhile(Char.IsLetterOrDigit, idx, endIdx);

			if (eIdx < 0)
				eIdx = endIdx + 1;

			if (eIdx - sIdx <= 0)
				return -1;

			op = src.Substring(sIdx, eIdx - sIdx).CastToLogicOperation();

			return eIdx;
		}
	}
}